summaryrefslogblamecommitdiffstatshomepage
path: root/frontend/src/routes/comics/[id]/+page.svelte
blob: cfc5840186c0e2fb680bff1babcb46a7380413fd (plain) (tree)















































































































































































                                                                                                                                              
<script lang="ts">
	import { beforeNavigate } from '$app/navigation';
	import { updateComics } from '$gql/Mutations';
	import { comicQuery } from '$gql/Queries';
	import { comicEquals } from '$gql/Utils';
	import { UpdateMode, type FullComicFragment, type UpdateComicInput } from '$gql/graphql';
	import { initReaderContext } from '$lib/Reader';
	import { initScraperContext } from '$lib/Scraper';
	import { initSelectionContext } from '$lib/Selection';
	import { setTabContext } from '$lib/Tabs';
	import { toastFinally } from '$lib/Toasts';
	import { preventOnPending } from '$lib/Utils';
	import BookmarkButton from '$lib/components/BookmarkButton.svelte';
	import Guard from '$lib/components/Guard.svelte';
	import Head from '$lib/components/Head.svelte';
	import OrganizedButton from '$lib/components/OrganizedButton.svelte';
	import RemovePageButton from '$lib/components/RemovePageButton.svelte';
	import SubmitButton from '$lib/components/SubmitButton.svelte';
	import Titlebar from '$lib/components/Titlebar.svelte';
	import Grid from '$lib/containers/Grid.svelte';
	import ComicForm from '$lib/forms/ComicForm.svelte';
	import Gallery from '$lib/gallery/Gallery.svelte';
	import PageView from '$lib/reader/PageView.svelte';
	import Reader from '$lib/reader/Reader.svelte';
	import ComicScrapeForm from '$lib/scraper/ComicScrapeForm.svelte';
	import ComicDelete from '$lib/tabs/ComicDelete.svelte';
	import ComicDetails from '$lib/tabs/ComicDetails.svelte';
	import Tab from '$lib/tabs/Tab.svelte';
	import Tabs from '$lib/tabs/Tabs.svelte';
	import SelectionControls from '$lib/toolbar/SelectionControls.svelte';
	import { getContextClient } from '@urql/svelte';
	import type { PageData } from './$types';

	const client = getContextClient();
	const reader = initReaderContext();
	const selection = initSelectionContext();
	const scraper = initScraperContext();
	const tabContext = setTabContext({
		tabs: {
			details: { title: 'Details' },
			edit: { title: 'Edit' },
			scrape: { title: 'Scrape' },
			deletion: { title: 'Delete' }
		},
		current: 'details'
	});

	export let data: PageData;
	$: result = comicQuery(client, { id: data.id });

	let comic: FullComicFragment;
	let original: Readonly<FullComicFragment>;
	let updatePartial = false;

	$: $result, update();
	$: pending = !comicEquals(comic, original);
	$: $tabContext.tabs.edit.badge = pending;

	function update() {
		if (!$result.stale && $result.data?.comic.__typename === 'FullComic') {
			original = $result.data.comic;
			if (updatePartial) {
				comic.pages = structuredClone(original.pages);
				comic.favourite = original.favourite;
				comic.bookmarked = original.bookmarked;
				comic.organized = original.organized;
				comic.updatedAt = original.updatedAt;
				updatePartial = false;
			} else {
				comic = structuredClone(original);
			}

			$reader.pages = original.pages;
			$selection.view = comic.pages;
			$scraper.selector = undefined;
		}
	}

	function toggle(field: keyof Omit<UpdateComicInput, 'cover'>) {
		updateComics(client, { ids: comic.id, input: { [field]: !comic[field] } })
			.then(() => (updatePartial = true))
			.catch(toastFinally);
	}

	function updateComic(event: CustomEvent<UpdateComicInput>) {
		updateComics(client, { ids: comic.id, input: event.detail }).catch(toastFinally);
	}

	function updateCover(event: CustomEvent<number>) {
		updateComics(client, { ids: comic.id, input: { cover: { id: event.detail } } })
			.then(() => (updatePartial = true))
			.catch(toastFinally);
	}

	function removePages() {
		updateComics(client, {
			ids: comic.id,
			input: { pages: { ids: $selection.ids, options: { mode: UpdateMode.Remove } } }
		})
			.then(() => {
				updatePartial = true;
				$selection = $selection.clear();
			})
			.catch(toastFinally);
	}

	beforeNavigate((navigation) => preventOnPending(navigation, pending));
</script>

<Head section="Comic" title={original?.title} />

{#if comic}
	<Grid>
		<header>
			<Titlebar
				title={original.title}
				subtitle={original.originalTitle}
				bind:favourite={comic.favourite}
				on:favourite={() => toggle('favourite')}
			/>
		</header>

		<aside>
			<Tabs>
				<Tab id="details">
					<ComicDetails comic={original} />
				</Tab>
				<Tab id="edit">
					<div class="flex flex-col gap-4">
						<div class="flex gap-2 text-sm">
							<SelectionControls page>
								<RemovePageButton on:click={removePages} />
							</SelectionControls>
							<div class="grow" />
							<BookmarkButton bookmarked={comic.bookmarked} on:click={() => toggle('bookmarked')} />
							<OrganizedButton organized={comic.organized} on:click={() => toggle('organized')} />
						</div>
						<ComicForm bind:comic on:submit={updateComic}>
							<div class="flex gap-2">
								<div class="grow" />
								<SubmitButton active={pending} />
							</div>
						</ComicForm>
					</div>
				</Tab>
				<Tab id="scrape">
					<ComicScrapeForm {comic} />
				</Tab>
				<Tab id="deletion">
					<ComicDelete {comic} />
				</Tab>
			</Tabs>
		</aside>

		<main class="overflow-auto">
			<Gallery
				pages={comic.pages}
				on:open={(e) => ($reader = $reader.open(e.detail))}
				on:cover={updateCover}
			/>
		</main>
	</Grid>

	<Reader>
		<PageView layout={comic.layout} direction={comic.direction} />
		<svelte:fragment slot="sidebar">
			<ComicForm bind:comic on:submit={updateComic}>
				<div class="flex justify-end gap-2">
					<SubmitButton active={pending} />
				</div>
			</ComicForm>
		</svelte:fragment>
	</Reader>
{:else}
	<Guard {result} />
{/if}